package com.spicesoft.clientobjects.core;

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.lang.reflect.Array;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

import com.spicesoft.clientobjects.BeanEnhancer;
import com.spicesoft.clientobjects.BeanRegistry;

public class PropertyInterceptor implements MethodInterceptor {

  private PropertyChangeSupport pcSupport;

  private BeanMetadata beanMeta;

  private Object original;

  private Map<String, Object> enhancedFields = new HashMap<String, Object>();

  private Map<String, PropertyChangeListener> enhancedFieldListeners = new HashMap<String, PropertyChangeListener>();

  private Map<String, PropertyChangeListener[]> enhancedIndexedFieldListeners = new HashMap<String, PropertyChangeListener[]>();

  public PropertyInterceptor(Object original) {
    this.original = original;
    this.pcSupport = new PropertyChangeSupport(this);
    this.beanMeta = BeanRegistry.getBeanMetadata(original.getClass());

    for (PropertyMetadata pm : this.beanMeta.getEnhancedProperties()) {
      if (pm.isIndexed()) {
        enhanceIndexedProperty(pm);
      } else {
        enhanceProperty(pm);
      }
    }
  }

  private synchronized void enhanceIndexedProperty(PropertyMetadata propertyMetadata) {
    try {
      final String propertyName = propertyMetadata.getPropertyName();
      
      // Clean up previous listeners if any
      Object oldEnhancedArray = this.enhancedFields.get(propertyName);
      if (oldEnhancedArray != null) {
        int length = Array.getLength(oldEnhancedArray);
        PropertyChangeListener[] listeners = this.enhancedIndexedFieldListeners.get(propertyName);
        for (int i = 0; i < length; i++) {
          Object enhancedElement = Array.get(oldEnhancedArray, i);
          if (enhancedElement != null) {
            ((PropertyChangeListenerSupport) enhancedElement).removePropertyChangeListener(listeners[i]);
          }
        }
        this.enhancedIndexedFieldListeners.remove(propertyName);
        this.enhancedFields.remove(propertyName);
      }
      
      // Enhance new array
      Object array = propertyMetadata.getGetterMethod().invoke(this.original);

      if (array == null) {
        return;
      }

      int length = Array.getLength(array);

      PropertyChangeListener[] listeners = new PropertyChangeListener[length];
      
      Object enhancedArray = Array.newInstance(propertyMetadata.getComponentType(), length);

      for (int i = 0; i < length; i++) {
        Object originalElementValue = Array.get(array, i);
        final int index = i;

        enhanceIndexedPropertyElement(propertyName, index, enhancedArray, originalElementValue, listeners);
      }

      this.enhancedFields.put(propertyName, enhancedArray);
      this.enhancedIndexedFieldListeners.put(propertyName, listeners);
    } catch (Exception e) {
      throw new RuntimeException("Unexpected error", e);
    }
  }

  private void enhanceIndexedPropertyElement(final String propertyName, final int index, Object enhancedArray,
      Object originalElementValue, PropertyChangeListener[] listeners) {
    
    if (originalElementValue == null) {
      return;
    }
    
    Object enhancedElementValue = BeanEnhancer.addPropertyChangeSupport(originalElementValue);

    // Wire a listener to the enhanced field
    PropertyChangeListenerSupport enhancedElementPCS = (PropertyChangeListenerSupport) enhancedElementValue;
    PropertyChangeListener enhancedElementListener = new PropertyChangeListener() {
      public void propertyChange(PropertyChangeEvent evt) {
        pcSupport.fireIndexedPropertyChange(propertyName, index, evt.getOldValue(), evt.getNewValue());
      }
    };
    enhancedElementPCS.addPropertyChangeListener(enhancedElementListener);
    listeners[index] = enhancedElementListener;
    Array.set(enhancedArray, index, enhancedElementValue);
  }

  private synchronized void enhanceProperty(PropertyMetadata propertyMetadata) {
    try {
      Object originalFieldValue = propertyMetadata.getGetterMethod().invoke(this.original);

      if (originalFieldValue == null) {
        return;
      }

      Object enhancedFieldValue = BeanEnhancer.addPropertyChangeSupport(originalFieldValue);
      final String propertyName = propertyMetadata.getPropertyName();

      // Unwire the old value and listener if any
      Object previousEnhancedValue = this.enhancedFields.get(propertyName);
      if (previousEnhancedValue != null) {
        PropertyChangeListenerSupport enhancedFieldPCS = (PropertyChangeListenerSupport) previousEnhancedValue;
        enhancedFieldPCS.removePropertyChangeListener(this.enhancedFieldListeners.get(propertyName));
        this.enhancedFieldListeners.remove(propertyName);
        this.enhancedFields.remove(propertyName);
      }

      // Wire a listener to the enhanced field
      PropertyChangeListenerSupport enhancedFieldPCS = (PropertyChangeListenerSupport) enhancedFieldValue;
      PropertyChangeListener enhancedFieldListener = new PropertyChangeListener() {
        public void propertyChange(PropertyChangeEvent evt) {
          PropertyChangeEvent event = new PropertyChangeEvent(evt.getSource(), propertyName, evt.getOldValue(), evt
              .getNewValue());
          pcSupport.firePropertyChange(event);
        }
      };
      enhancedFieldPCS.addPropertyChangeListener(enhancedFieldListener);

      this.enhancedFields.put(propertyName, enhancedFieldValue);
      this.enhancedFieldListeners.put(propertyName, enhancedFieldListener);
    } catch (Exception e) {
      throw new RuntimeException("Unexpected error", e);
    }
  }

  public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
    if (MethodUtils.isPropertyChangeListenerAdder(method)) {
      if (args.length == 1) {
        pcSupport.addPropertyChangeListener((PropertyChangeListener) args[0]);
      } else {
        pcSupport.addPropertyChangeListener((String) args[0], (PropertyChangeListener) args[1]);
      }
      return null;
    }

    if (MethodUtils.isPropertyChangeListenerRemover(method)) {
      if (args.length == 1) {
        pcSupport.removePropertyChangeListener((PropertyChangeListener) args[0]);
      } else {
        pcSupport.removePropertyChangeListener((String) args[0], (PropertyChangeListener) args[1]);
      }
      return null;
    }

    PropertyMetadata pm;

    if ((pm = this.beanMeta.getPropertyBySetter(method)) != null) {
      Object oldValue = pm.getGetterMethod().invoke(obj, (Object[]) null);

      method.invoke(original, args);

      if (this.beanMeta.getEnhancedProperties().contains(pm)) {
        enhanceProperty(pm);
      }

      PropertyChangeEvent event = new PropertyChangeEvent(obj, pm.getPropertyName(), oldValue, args[0]);
      pcSupport.firePropertyChange(event);

      return null;
    }

    if ((pm = this.beanMeta.getPropertyByGetter(method)) != null) {
      String propertyName = pm.getPropertyName();
      Object enhancedField = this.enhancedFields.get(propertyName);
      if (enhancedField != null) {
        return enhancedField;
      } else {
        return method.invoke(this.original, (Object[]) null);
      }
    }

    if ((pm = this.beanMeta.getPropertyByIndexedSetter(method)) != null) {
      Object oldValue = pm.getIndexedGetterMethod().invoke(obj, args[0]);
      Integer index = (Integer) args[0];

      PropertyChangeListener[] listeners = this.enhancedIndexedFieldListeners.get(pm.getPropertyName());
      if (pm.isEnhanced() && oldValue != null) {
        // Remove old listener
        PropertyChangeListener listener = listeners[index];
        if (listener != null) {
          ((PropertyChangeListenerSupport)oldValue).removePropertyChangeListener(listener);
          listeners[index] = null;
        }
      }
      
      method.invoke(original, args);
      
      if (pm.isEnhanced()) {
        enhanceIndexedPropertyElement(pm.getPropertyName(), index,
            this.enhancedFields.get(pm.getPropertyName()), args[1], listeners);
      }

      pcSupport.fireIndexedPropertyChange(pm.getPropertyName(), index, oldValue, args[1]);

      return null;
    }

    if ((pm = this.beanMeta.getPropertyByIndexedGetter(method)) != null) {
      Integer index = (Integer) args[0];
      String propertyName = pm.getPropertyName();
      Object enhancedField = this.enhancedFields.get(propertyName);
      if (enhancedField != null) {
        return Array.get(enhancedField, index);
      } else {
        return method.invoke(this.original, args);
      }
    }

    return proxy.invokeSuper(obj, args);
  }

}
